"""
Elasticsearch索引迁移工具
用于不同ES版本间的索引结构迁移（支持ES6->ES6、ES6->ES7等）
"""

import json
import logging
import sys
import os
import urllib3
from datetime import datetime
from logging.handlers import RotatingFileHandler
from typing import Dict, List, Any, Optional, Tuple
from elasticsearch import Elasticsearch
from elasticsearch.exceptions import ConnectionError, NotFoundError, RequestError

from config import (
    SOURCE_ES_CONFIG, TARGET_ES_CONFIG, EXPORT_CONFIG, 
    LOGGING_CONFIG, MIGRATION_CONFIG
)

# 关闭SSL警告
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)


class ESIndexMigrator:
    """ES索引迁移工具主类"""
    
    def __init__(self, source_config: Dict = None, target_config: Dict = None):
        """
        初始化迁移工具
        
        Args:
            source_config: 源ES配置，默认使用config.py中的配置
            target_config: 目标ES配置，默认使用config.py中的配置
        """
        self.source_config = source_config or SOURCE_ES_CONFIG
        self.target_config = target_config or TARGET_ES_CONFIG
        
        # 设置日志
        self._setup_logging()
        
        # 初始化ES客户端和版本信息
        self.source_es = None
        self.target_es = None
        self.source_version = None
        self.target_version = None
        self.source_major_version = None
        self.target_major_version = None
        
        # 连接测试
        self._connect_elasticsearch()
        
        # 检测版本兼容性
        self._analyze_version_compatibility()
        
        # 检查目标ES的分析器插件状态
        self._check_target_analyzers()
        
    def _setup_logging(self):
        """设置日志配置"""
        # 确保日志目录存在
        log_dir = os.path.dirname(LOGGING_CONFIG['file'])
        if not os.path.exists(log_dir):
            os.makedirs(log_dir)
            
        # 创建RotatingFileHandler
        file_handler = RotatingFileHandler(
            filename=LOGGING_CONFIG['file'],
            maxBytes=LOGGING_CONFIG['max_size'],
            backupCount=LOGGING_CONFIG['backup_count'],
            encoding=LOGGING_CONFIG['encoding']
        )
        
        # 创建控制台处理器
        console_handler = logging.StreamHandler(sys.stdout)
        
        # 设置格式化器
        formatter = logging.Formatter(LOGGING_CONFIG['format'])
        file_handler.setFormatter(formatter)
        console_handler.setFormatter(formatter)
        
        # 配置根日志记录器
        self.logger = logging.getLogger(__name__)
        self.logger.setLevel(getattr(logging, LOGGING_CONFIG['level']))
        
        # 移除所有现有的处理器
        for handler in self.logger.handlers[:]:
            self.logger.removeHandler(handler)
        
        # 添加新的处理器
        self.logger.addHandler(file_handler)
        self.logger.addHandler(console_handler)
        
        # 设置不传播到父记录器
        self.logger.propagate = False
    
    def _build_es_connection_string(self, config: Dict) -> str:
        """构建ES连接字符串"""
        # 检查是否是完整URL
        host = config.get('host', '')
        if '://' in host:
            # 如果host包含完整URL，直接使用它
            url = host
            if config.get('username') and config.get('password'):
                # 在URL中插入认证信息
                scheme, rest = url.split('://', 1)
                url = f"{scheme}://{config['username']}:{config['password']}@{rest}"
            
            # 如果有额外的path，添加到URL中
            if config.get('path'):
                path = config.get('path')
                # 保持原始格式
                url = url.rstrip('/') + '/' + path.strip('/')
            return url

        # 使用分离参数构建URL
        scheme = config.get('scheme', 'http')
        host = config.get('host', 'localhost')
        port = config.get('port')
        username = config.get('username')
        password = config.get('password')
        path = config.get('path', '')

        # 构建基础URL
        url = f"{scheme}://"

        # 添加认证信息
        if username and password:
            url += f"{username}:{password}@"

        # 添加主机
        url += host

        # 只有在没有path时才添加端口号
        if not path and port:
            try:
                port = int(port)
                url += f":{port}"
            except (TypeError, ValueError):
                # 如果端口号无效，使用默认端口
                default_port = 443 if scheme == 'https' else 9200
                url += f":{default_port}"

        # 添加路径，完全保持原始格式
        if path:
            url = url.rstrip('/') + '/' + path.strip('/')

        return url
    
    def _connect_elasticsearch(self):
        """连接到源和目标ES集群"""
        try:
            # 连接源ES
            source_url = self._build_es_connection_string(self.source_config)
            self.logger.info(f"正在连接源ES: {source_url}")
            
            # 确保超时值是整数
            default_timeout = 30
            source_timeout = self.source_config.get('timeout')
            try:
                source_timeout = float(source_timeout) if source_timeout is not None else default_timeout
                if source_timeout <= 0:
                    source_timeout = default_timeout
            except (TypeError, ValueError):
                source_timeout = default_timeout
            
            # 准备基本参数
            source_params = {
                'hosts': [source_url],
                'headers': {
                    'Accept': 'application/json',
                    'Content-Type': 'application/json',
                    'User-Agent': 'ES-Migration-Tool/1.0',
                    'X-Elastic-Product': 'Elasticsearch'
                }
            }
            
            # 添加超时和重试参数
            try:
                source_params.update({
                    'request_timeout': int(source_timeout),
                    'max_retries': int(self.source_config.get('max_retries', 3)),
                    'retry_on_timeout': bool(self.source_config.get('retry_on_timeout', True))
                })
            except (TypeError, ValueError) as e:
                self.logger.warning(f"解析超时和重试参数时出错，使用默认值: {e}")
                source_params.update({
                    'request_timeout': 30,
                    'max_retries': 3,
                    'retry_on_timeout': True
                })
            
            # 添加SSL参数
            try:
                source_params.update({
                    'verify_certs': bool(self.source_config.get('verify_certs', False)),
                    'ssl_show_warn': bool(self.source_config.get('ssl_show_warn', False))
                })
            except (TypeError, ValueError) as e:
                self.logger.warning(f"解析SSL参数时出错，使用默认值: {e}")
                source_params.update({
                    'verify_certs': False,
                    'ssl_show_warn': False
                })

            # 禁用嗅探和自动发现
            source_params.update({
                'sniff_on_start': False,
                'sniff_on_connection_fail': False,
                'sniffer_timeout': 60,
                'sniff_timeout': 10,
                'http_compress': True
            })

            # 如果是HTTPS，添加SSL配置
            if str(self.source_config.get('scheme')).lower() == 'https':
                source_params.update({
                    'ssl_assert_hostname': False,
                    'ssl_assert_fingerprint': None,
                    'ssl_version': None,
                    'ssl_context': None
                })
            
            self.source_es = Elasticsearch(**source_params)

            # 尝试获取版本信息
            try:
                source_info = self.source_es.info()
                self.source_version = source_info['version']['number']
                self.source_major_version = int(self.source_version.split('.')[0])
                self.logger.info(f"源ES连接成功，版本: {self.source_version}")
            except Exception as e:
                # 如果info()调用失败，尝试直接发送HTTP请求
                import requests
                from requests.exceptions import RequestException
                
                try:
                    # 构建完整URL
                    full_url = source_url
                    if not full_url.endswith('/'):
                        full_url += '/'
                    
                    # 准备认证信息
                    auth = None
                    if self.source_config.get('username') and self.source_config.get('password'):
                        auth = (self.source_config['username'], self.source_config['password'])
                    
                    # 准备请求头
                    headers = {
                        'Accept': 'application/json',
                        'Content-Type': 'application/json',
                        'User-Agent': 'ES-Migration-Tool/1.0',
                        'X-Elastic-Product': 'Elasticsearch'
                    }
                    
                    # 发送请求并打印详细信息
                    self.logger.info(f"尝试直接HTTP请求: {full_url}")
                    response = requests.get(
                        full_url,
                        auth=auth,
                        headers=headers,
                        verify=False,
                        timeout=30
                    )
                    
                    # 打印响应信息以便诊断
                    self.logger.info(f"HTTP状态码: {response.status_code}")
                    self.logger.info(f"响应头: {dict(response.headers)}")
                    self.logger.info(f"响应内容: {response.text[:500]}...")  # 只打印前500个字符
                    
                    if response.ok:
                        try:
                            info = response.json()
                            if 'version' in info and 'number' in info['version']:
                                self.source_version = info['version']['number']
                                self.source_major_version = int(self.source_version.split('.')[0])
                                self.logger.info(f"源ES连接成功，版本: {self.source_version}")
                            else:
                                raise ValueError("响应中没有版本信息")
                        except ValueError as ve:
                            self.logger.error(f"解析响应失败: {ve}")
                            raise
                    else:
                        response.raise_for_status()
                        
                except RequestException as re:
                    self.logger.error(f"HTTP请求失败: {re}")
                    raise
            
            # 连接目标ES
            target_url = self._build_es_connection_string(self.target_config)
            self.logger.info(f"正在连接目标ES: {target_url}")
            
            # 确保超时值是整数
            target_timeout = self.target_config.get('timeout')
            try:
                target_timeout = float(target_timeout) if target_timeout is not None else default_timeout
                if target_timeout <= 0:
                    target_timeout = default_timeout
            except (TypeError, ValueError):
                target_timeout = default_timeout
            
            # 准备基本参数
            target_params = {
                'hosts': [target_url],
                'headers': {
                    'Accept': 'application/json',
                    'Content-Type': 'application/json',
                    'User-Agent': 'ES-Migration-Tool/1.0'
                }
            }
            
            # 添加超时和重试参数
            try:
                target_params.update({
                    'request_timeout': int(target_timeout),
                    'max_retries': int(self.target_config.get('max_retries', 3)),
                    'retry_on_timeout': bool(self.target_config.get('retry_on_timeout', True))
                })
            except (TypeError, ValueError) as e:
                self.logger.warning(f"解析超时和重试参数时出错，使用默认值: {e}")
                target_params.update({
                    'request_timeout': 30,
                    'max_retries': 3,
                    'retry_on_timeout': True
                })
            
            # 添加SSL参数
            try:
                target_params.update({
                    'verify_certs': bool(self.target_config.get('verify_certs', False)),
                    'ssl_show_warn': bool(self.target_config.get('ssl_show_warn', False))
                })
            except (TypeError, ValueError) as e:
                self.logger.warning(f"解析SSL参数时出错，使用默认值: {e}")
                target_params.update({
                    'verify_certs': False,
                    'ssl_show_warn': False
                })

            # 禁用嗅探和自动发现
            target_params.update({
                'sniff_on_start': False,
                'sniff_on_connection_fail': False,
                'sniffer_timeout': 60,
                'sniff_timeout': 10,
                'http_compress': True
            })

            # 如果是HTTPS，添加SSL配置
            if str(self.target_config.get('scheme')).lower() == 'https':
                target_params.update({
                    'ssl_assert_hostname': False,
                    'ssl_assert_fingerprint': None,
                    'ssl_version': None,
                    'ssl_context': None
                })
            
            # 添加SSL证书配置
            if self.target_config.get('ca_certs'):
                target_params['ca_certs'] = self.target_config['ca_certs']
            if self.target_config.get('client_cert'):
                target_params['client_cert'] = self.target_config['client_cert']
            if self.target_config.get('client_key'):
                target_params['client_key'] = self.target_config['client_key']
            
            self.target_es = Elasticsearch(**target_params)
            
            # 尝试获取版本信息
            try:
                target_info = self.target_es.info(request_timeout=int(target_timeout))
                self.target_version = target_info['version']['number']
                self.target_major_version = int(self.target_version.split('.')[0])
                self.logger.info(f"目标ES连接成功，版本: {self.target_version}")
            except Exception as e:
                # 如果info()调用失败，尝试直接发送HTTP请求
                import requests
                from requests.exceptions import RequestException
                
                try:
                    # 构建完整URL
                    full_url = target_url
                    if not full_url.endswith('/'):
                        full_url += '/'
                    
                    # 准备认证信息
                    auth = None
                    if self.target_config.get('username') and self.target_config.get('password'):
                        auth = (self.target_config['username'], self.target_config['password'])
                    
                    # 发送请求
                    response = requests.get(
                        full_url,
                        auth=auth,
                        verify=False,
                        timeout=int(target_timeout),
                        headers={'Accept': 'application/json'}
                    )
                    response.raise_for_status()
                    
                    # 解析响应
                    info = response.json()
                    if 'version' in info and 'number' in info['version']:
                        self.target_version = info['version']['number']
                        self.target_major_version = int(self.target_version.split('.')[0])
                        self.logger.info(f"目标ES连接成功（通过HTTP请求），版本: {self.target_version}")
                    else:
                        raise Exception("无法从响应中获取版本信息")
                except RequestException as re:
                    raise Exception(f"HTTP请求失败: {str(re)}")
                except Exception as ex:
                    raise Exception(f"处理响应失败: {str(ex)}")
            
        except ConnectionError as e:
            self.logger.error(f"ES连接失败: {e}")
            raise
        except Exception as e:
            self.logger.error(f"连接过程中发生未知错误: {e}")
            raise
    
    def _analyze_version_compatibility(self):
        """分析版本兼容性并输出迁移策略"""
        self.logger.info("="*60)
        self.logger.info("版本兼容性分析:")
        self.logger.info(f"源ES版本: {self.source_version} (主版本: {self.source_major_version})")
        self.logger.info(f"目标ES版本: {self.target_version} (主版本: {self.target_major_version})")
        
        if self.source_major_version == self.target_major_version:
            if self.source_major_version == 6:
                self.logger.info("迁移策略: ES6 -> ES6，保持原有索引结构不变")
                self.migration_strategy = "es6_to_es6"
            elif self.source_major_version == 7:
                self.logger.info("迁移策略: ES7 -> ES7，保持原有索引结构不变")
                self.migration_strategy = "es7_to_es7"
            else:
                self.logger.info(f"迁移策略: ES{self.source_major_version} -> ES{self.target_major_version}，保持原有索引结构不变")
                self.migration_strategy = "same_version"
        elif self.source_major_version == 6 and self.target_major_version == 7:
            self.logger.info("迁移策略: ES6 -> ES7，需要转换索引结构（移除类型，调整映射格式）")
            self.migration_strategy = "es6_to_es7"
        elif self.source_major_version == 7 and self.target_major_version == 8:
            self.logger.info("迁移策略: ES7 -> ES8，可能需要小幅调整")
            self.migration_strategy = "es7_to_es8"
        else:
            self.logger.warning(f"迁移策略: 跨版本迁移 ES{self.source_major_version} -> ES{self.target_major_version}，可能需要自定义处理")
            self.migration_strategy = "cross_version"
        
        self.logger.info("="*60)
    
    def _needs_mapping_conversion(self) -> bool:
        """判断是否需要进行映射转换"""
        return self.migration_strategy == "es6_to_es7"
    
    def _needs_settings_conversion(self) -> bool:
        """判断是否需要进行设置转换"""
        return self.migration_strategy in ["es6_to_es7", "es7_to_es8"]
    
    def _should_exclude_index(self, index_name: str) -> bool:
        """检查索引是否应该被排除"""
        if not EXPORT_CONFIG.get('include_system_indices', False):
            if index_name.startswith('.'):
                return True
        
        for pattern in EXPORT_CONFIG.get('excluded_indices', []):
            if pattern.endswith('*'):
                if index_name.startswith(pattern[:-1]):
                    return True
            elif index_name == pattern:
                return True
        
        return False
    
    def export_index_structures(self, output_file: str = None) -> Dict:
        """
        导出源ES中的所有索引结构
        
        Args:
            output_file: 输出文件路径，默认使用配置文件中的路径
            
        Returns:
            Dict: 包含所有索引结构的字典
        """
        # 如果没有提供输出文件，生成一个基于源和目标ES的文件名
        if not output_file:
            # 从源ES URL中提取主机和端口
            source_host = self.source_config.get('host', '').replace('.', '_')
            source_port = self.source_config.get('port', '')
            
            # 从目标ES URL中提取主机和端口
            target_host = self.target_config.get('host', '').replace('.', '_')
            target_port = self.target_config.get('port', '')
            target_path = self.target_config.get('path', '').replace('/', '_').strip('_')
            
            # 构建文件名
            filename = f"preview_{source_host}_{source_port}_to_{target_host}_{target_port}"
            if target_path:
                filename += f"_{target_path}"
            filename += ".json"
            
            # 使用配置中的输出目录
            output_dir = EXPORT_CONFIG.get('output_dir', 'data/es_structures')
            output_file = os.path.join(output_dir, filename)
        
        # 确保输出目录存在
        output_dir = os.path.dirname(output_file)
        if output_dir and not os.path.exists(output_dir):
            os.makedirs(output_dir)
            
        self.logger.info(f"📁 预览文件: {os.path.basename(output_file)}")
        
        self.logger.info("开始导出索引结构...")
        
        try:
            # 获取所有索引
            try:
                self.logger.info("正在获取索引列表...")
                indices = self.source_es.indices.get_alias()
                self.logger.info(f"成功获取到 {len(indices)} 个索引")
            except Exception as e:
                self.logger.error(f"获取索引列表失败: {e}")
                raise

            exported_indices = {}
            exported_count = 0
            
            for index_name in indices.keys():
                if self._should_exclude_index(index_name):
                    self.logger.debug(f"跳过索引: {index_name}")
                    continue
                
                self.logger.info(f"正在导出索引: {index_name}")
                
                try:
                    # 获取索引映射和设置
                    mapping = self.source_es.indices.get_mapping(index=index_name)
                    settings = self.source_es.indices.get_settings(index=index_name)
                    
                    exported_indices[index_name] = {
                        'mappings': mapping[index_name]['mappings'],
                        'settings': settings[index_name]['settings'],
                        'export_time': datetime.now().isoformat(),
                        'source_version': self.source_version
                    }
                    exported_count += 1
                    self.logger.debug(f"成功导出索引 {index_name} 的结构")
                    
                except Exception as e:
                    self.logger.error(f"导出索引 {index_name} 失败: {e}")
                    continue
            
            # 保存到文件
            try:
                if not exported_indices:
                    self.logger.warning("没有找到可导出的索引")
                    return {}

                # 确保输出目录存在
                output_dir = os.path.dirname(output_file)
                if output_dir and not os.path.exists(output_dir):
                    os.makedirs(output_dir)

                with open(output_file, 'w', encoding='utf-8') as f:
                    json.dump(exported_indices, f, indent=2, ensure_ascii=False)
                
                self.logger.info(f"✅ 索引结构导出完成，共导出 {exported_count} 个索引")
                return exported_indices
            except Exception as e:
                self.logger.error(f"❌ 保存文件失败: {str(e)}")
                raise
            
        except Exception as e:
            self.logger.error(f"导出过程中发生错误: {e}")
            raise
    
    def convert_mapping(self, mapping: Dict) -> Dict:
        """
        根据目标ES版本转换映射结构
        
        Args:
            mapping: 源ES的映射结构
            
        Returns:
            Dict: 目标ES兼容的映射结构
        """
        # 如果不需要转换，直接返回原映射
        if not self._needs_mapping_conversion():
            self.logger.debug(f"迁移策略为 {self.migration_strategy}，保持原有映射结构不变")
            return mapping.copy() if isinstance(mapping, dict) else {}
        
        # 需要进行ES6到ES7的转换
        self.logger.debug("正在进行ES6到ES7的映射转换...")
        converted_mapping = {}
        
        if not isinstance(mapping, dict):
            return converted_mapping
        
        # 情况1: mapping中包含类型定义（如 _doc）
        type_found = False
        for key, value in mapping.items():
            if isinstance(value, dict) and 'properties' in value:
                # 这是一个类型定义，提取properties
                converted_mapping = {
                    'properties': value['properties']
                }
                # 保留其他映射级别的设置（如dynamic等）
                for setting_key, setting_value in value.items():
                    if setting_key != 'properties':
                        converted_mapping[setting_key] = setting_value
                type_found = True
                break
        
        # 情况2: 如果没有类型定义，检查是否已经有properties结构
        if not type_found:
            if 'properties' in mapping:
                # 已经是正确的ES7格式
                converted_mapping = mapping.copy()
            else:
                # 情况3: 字段直接定义在根级别，需要包装到properties中
                # 检查是否有字段定义（包含type属性的对象）
                has_field_definitions = False
                properties = {}
                other_settings = {}
                
                for key, value in mapping.items():
                    if isinstance(value, dict) and ('type' in value or 'properties' in value):
                        # 这是字段定义
                        properties[key] = value
                        has_field_definitions = True
                    else:
                        # 这是映射级别的设置
                        other_settings[key] = value
                
                if has_field_definitions:
                    converted_mapping = {'properties': properties}
                    # 添加其他映射级别的设置
                    converted_mapping.update(other_settings)
                else:
                    # 如果没有字段定义，直接使用原映射
                    converted_mapping = mapping.copy()
        
        # 移除ES7中不支持的映射参数
        self._remove_deprecated_mapping_params(converted_mapping)
        
        return converted_mapping
    
    def _remove_deprecated_mapping_params(self, mapping: Dict):
        """移除ES7中已弃用的映射参数并处理分析器兼容性"""
        deprecated_params = [
            'include_in_all',  # ES7中移除了_all字段
            '_all',
            'boost'  # 某些boost参数在ES7中被弃用
        ]
        
        if isinstance(mapping, dict):
            for param in deprecated_params:
                mapping.pop(param, None)
            
            # 处理分析器兼容性
            self._handle_analyzer_compatibility(mapping)
            
            # 递归处理properties
            if 'properties' in mapping:
                for field_name, field_mapping in mapping['properties'].items():
                    if isinstance(field_mapping, dict):
                        self._remove_deprecated_mapping_params(field_mapping)

    def _handle_analyzer_compatibility(self, field_mapping: Dict):
        """
        处理分析器兼容性问题
        将不支持的分析器替换为兼容的分析器
        """
        # 定义分析器映射规则
        analyzer_mapping = {
            # IK中文分词器映射
            'ik_smart': 'standard',          # 智能分词 -> 标准分词
            'ik_max_word': 'standard',       # 细粒度分词 -> 标准分词
            
            # 其他常见的自定义分析器映射
            'whitespace_analyzer': 'whitespace',
            'keyword_analyzer': 'keyword',
            'simple_analyzer': 'simple',
            
            # 如果遇到其他未知分析器，也映射到standard
        }
        
        if isinstance(field_mapping, dict):
            # 检查字段级别的分析器
            if 'analyzer' in field_mapping:
                original_analyzer = field_mapping['analyzer']
                if original_analyzer in analyzer_mapping:
                    new_analyzer = analyzer_mapping[original_analyzer]
                    self.logger.warning(
                        f"⚠️ 分析器替换: '{original_analyzer}' -> '{new_analyzer}' "
                        f"(原分析器在目标ES中不可用)"
                    )
                    field_mapping['analyzer'] = new_analyzer
                elif not self._is_builtin_analyzer(original_analyzer):
                    # 如果是未知的自定义分析器，替换为standard
                    self.logger.warning(
                        f"⚠️ 未知分析器替换: '{original_analyzer}' -> 'standard' "
                        f"(原分析器在目标ES中不可用)"
                    )
                    field_mapping['analyzer'] = 'standard'
            
            # 检查搜索时分析器
            if 'search_analyzer' in field_mapping:
                original_analyzer = field_mapping['search_analyzer']
                if original_analyzer in analyzer_mapping:
                    new_analyzer = analyzer_mapping[original_analyzer]
                    self.logger.warning(
                        f"⚠️ 搜索分析器替换: '{original_analyzer}' -> '{new_analyzer}'"
                    )
                    field_mapping['search_analyzer'] = new_analyzer
                elif not self._is_builtin_analyzer(original_analyzer):
                    self.logger.warning(
                        f"⚠️ 未知搜索分析器替换: '{original_analyzer}' -> 'standard'"
                    )
                    field_mapping['search_analyzer'] = 'standard'
            
            # 递归处理嵌套对象和字段
            if 'properties' in field_mapping:
                for nested_field_name, nested_field_mapping in field_mapping['properties'].items():
                    if isinstance(nested_field_mapping, dict):
                        self._handle_analyzer_compatibility(nested_field_mapping)

    def _is_builtin_analyzer(self, analyzer_name: str) -> bool:
        """
        检查是否为ES内置分析器
        """
        builtin_analyzers = {
            'standard', 'simple', 'whitespace', 'stop', 'keyword', 
            'pattern', 'language', 'fingerprint', 'custom'
        }
        return analyzer_name in builtin_analyzers

    def _check_target_analyzers(self):
        """
        检查目标ES环境中可用的分析器
        """
        try:
            # 获取目标ES的分析器信息
            response = self.target_es.cat.plugins(format='json', request_timeout=5)
            installed_plugins = [plugin['component'] for plugin in response] if response else []
            
            has_ik = any('analysis-ik' in plugin or 'ik' in plugin.lower() for plugin in installed_plugins)
            
            if not has_ik:
                self.logger.warning(
                    "⚠️ 目标ES环境未安装IK中文分词器插件，将自动替换相关分析器"
                )
                self.logger.info(
                    "💡 建议安装IK插件以获得更好的中文分词效果: "
                    "https://github.com/medcl/elasticsearch-analysis-ik"
                )
            else:
                self.logger.info("✅ 检测到目标ES已安装IK中文分词器插件")
                
        except Exception as e:
            self.logger.warning(f"⚠️ 无法检查目标ES插件状态: {e}")
            self.logger.info("💡 将使用默认分析器替换策略")
    
    def convert_settings(self, settings: Dict) -> Dict:
        """
        根据目标ES版本转换设置结构
        
        Args:
            settings: 源ES的设置结构
            
        Returns:
            Dict: 目标ES兼容的设置结构
        """
        # 如果不需要转换，直接返回原设置（但要过滤掉只读设置）
        if not self._needs_settings_conversion():
            self.logger.debug(f"迁移策略为 {self.migration_strategy}，保持原有设置结构")
            # 即使是同版本迁移，也需要移除只读设置
            return self._remove_readonly_settings(settings)
        
        # 需要进行版本转换
        self.logger.debug(f"正在进行 {self.migration_strategy} 的设置转换...")
        converted_settings = {}
        
        if 'index' in settings:
            index_settings = settings['index'].copy()
            
            # 移除ES7中不支持的设置
            deprecated_settings = [
                'creation_date',  # 这些是只读设置，创建索引时不能设置
                'uuid',
                'version',
                'provided_name'
            ]
            
            for setting in deprecated_settings:
                index_settings.pop(setting, None)
            
            converted_settings['index'] = index_settings
        
        return converted_settings
    
    def _remove_readonly_settings(self, settings: Dict) -> Dict:
        """移除只读设置，这些设置在创建索引时不能设置"""
        converted_settings = {}
        
        if 'index' in settings:
            index_settings = settings['index'].copy()
            
            # 移除只读设置
            readonly_settings = [
                'creation_date',
                'uuid',
                'version',
                'provided_name'
            ]
            
            for setting in readonly_settings:
                index_settings.pop(setting, None)
            
            converted_settings['index'] = index_settings
        
        return converted_settings
    
    def import_index_structures(self, input_file: str = None) -> Dict:
        """
        在目标ES中创建索引
        
        Args:
            input_file: 导入文件路径，默认使用配置文件中的路径
            
        Returns:
            Dict: 导入结果统计
        """
        input_file = input_file or EXPORT_CONFIG['output_file']
        self.logger.info(f"开始从文件导入索引结构: {input_file}")
        
        try:
            # 读取导出的索引结构
            with open(input_file, 'r', encoding='utf-8') as f:
                exported_indices = json.load(f)
            
            results = {
                'success': [],
                'failed': [],
                'skipped': []
            }
            
            for index_name, index_data in exported_indices.items():
                self.logger.info(f"正在创建索引: {index_name}")
                
                try:
                    # 检查索引是否已存在
                    if self.target_es.indices.exists(index=index_name):
                        if not MIGRATION_CONFIG.get('create_if_exists', False):
                            self.logger.warning(f"索引 {index_name} 已存在，跳过创建")
                            results['skipped'].append(index_name)
                            continue
                        else:
                            if MIGRATION_CONFIG.get('backup_existing', True):
                                backup_name = f"{index_name}_backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
                                self.logger.info(f"备份现有索引到: {backup_name}")
                                # 这里可以添加备份逻辑
                            
                            self.logger.info(f"删除现有索引: {index_name}")
                            self.target_es.indices.delete(index=index_name)
                    
                    # 根据版本策略转换映射和设置
                    self.logger.info(f"正在处理索引 {index_name}，迁移策略: {self.migration_strategy}")
                    converted_mapping = self.convert_mapping(index_data['mappings'])
                    converted_settings = self.convert_settings(index_data['settings'])
                    
                    # 构建创建索引的body
                    create_body = {}
                    
                    if converted_mapping:
                        create_body['mappings'] = converted_mapping
                    
                    if converted_settings:
                        create_body['settings'] = converted_settings
                    
                    # 预览模式
                    if MIGRATION_CONFIG.get('dry_run', False):
                        self.logger.info(f"[预览模式] 将创建索引 {index_name}")
                        self.logger.debug(f"[预览模式] 索引配置: {json.dumps(create_body, indent=2)}")
                        results['success'].append(index_name)
                        continue
                    
                    # 创建索引
                    self.target_es.indices.create(
                        index=index_name,
                        body=create_body
                    )
                    
                    self.logger.info(f"索引 {index_name} 创建成功")
                    results['success'].append(index_name)
                    
                except RequestError as e:
                    self.logger.error(f"创建索引 {index_name} 失败 (请求错误): {e}")
                    results['failed'].append({'index': index_name, 'error': str(e)})
                except Exception as e:
                    self.logger.error(f"创建索引 {index_name} 失败: {e}")
                    results['failed'].append({'index': index_name, 'error': str(e)})
            
            # 保存到文件
            try:
                with open(output_file, 'w', encoding='utf-8') as f:
                    json.dump(exported_indices, f, indent=2, ensure_ascii=False)
                self.logger.info(f"✅ 成功导出 {exported_count} 个索引的结构到文件: {output_file}")
            except Exception as e:
                self.logger.error(f"❌ 写入文件失败: {e}")
                raise
            
            return exported_indices
            
        except FileNotFoundError:
            self.logger.error(f"找不到导入文件: {input_file}")
            raise
        except json.JSONDecodeError as e:
            self.logger.error(f"导入文件格式错误: {e}")
            raise
        except Exception as e:
            self.logger.error(f"导入过程中发生错误: {e}")
            raise
    
    def migrate_all(self, output_file: str = None) -> Dict:
        """
        执行完整的迁移流程：导出 -> 转换 -> 导入
        
        Args:
            output_file: 中间文件路径
            
        Returns:
            Dict: 迁移结果
        """
        self.logger.info("开始执行完整迁移流程...")
        
        try:
            # 步骤1: 导出索引结构
            self.logger.info("步骤1: 导出源ES索引结构")
            exported_data = self.export_index_structures(output_file)
            
            # 步骤2: 导入索引结构
            self.logger.info("步骤2: 在目标ES中创建索引")
            import_results = self.import_index_structures(output_file)
            
            self.logger.info("完整迁移流程执行完成！")
            return {
                'exported_count': len(exported_data),
                'import_results': import_results
            }
            
        except Exception as e:
            self.logger.error(f"迁移过程中发生错误: {e}")
            raise


if __name__ == "__main__":
    """主程序入口"""
    try:
        # 创建迁移工具实例
        migrator = ESIndexMigrator()
        
        # 执行完整迁移
        results = migrator.migrate_all()
        
        print("\n" + "="*50)
        print("迁移完成！")
        print(f"导出索引数量: {results['exported_count']}")
        print(f"成功创建索引: {len(results['import_results']['success'])}")
        print(f"失败索引: {len(results['import_results']['failed'])}")
        print(f"跳过索引: {len(results['import_results']['skipped'])}")
        print("="*50)
        
    except KeyboardInterrupt:
        print("\n用户中断迁移过程")
    except Exception as e:
        print(f"\n迁移失败: {e}")
        sys.exit(1) 